Weaving maps of multivariate data
Weave patterns where each ‘thread’ or ‘strand’ represents a different attribute that can be independently symbolised in a map.
from Chaves LF, MD Friberg, LA Hurtado, R Marín Rodríguez, D O’Sullivan, and LR Bergmann. 2021 (online first). Trade, uneven development and people in motion: Used territories and the initial spread of COVID-19 in Mesoamerica and the Caribbean. Socio-Economic Planning Sciences.
This was done with SVG symbol fills in QGIS, and was (very!) fiddly to produce.
Motivation and other approaches
Increasingly, we deal with highly multivariate data.
Many approaches can be used to visualise these spatially, but it’s always challenging. We may resort to small multiple displays, where each attribute is presented as an individual (small) map. Or perhaps more often, we flip back and forward among many layers in a GIS.
Wouldn’t it be nice to see multiple attributes together?! Perhaps to be able to identify patterns across more than one attribute in combination.
We’re not the first to think this, so there are plenty of approaches already around…
Small multiples
This is sf’s default plot output for a dataset.
plot(region)
Bivariate choropleths
A crude approach uses alpha values for each attribute, e.g., in tmap.
tm_shape(region) +
tm_polygons(col = "dose2_uptake", palette = "-Reds", alpha = 0.65,
title = "Dose 2 per thousand ") +
tm_shape(region) +
tm_polygons(col = "pAsian", palette = "Blues", alpha = 0.35,
title = "% Asian") +
tm_layout(legend.outside = TRUE)
Or, use something like Jan Caha’s QGIS plugin which implements the approach described by Joshua Stevens in this post
Bivariate choropleth made in QGIS
Trivariate choropleths
Mixing three colours is hard, but e.g., the tricolore package can do this…
eth_mix <- tricolore::Tricolore(
region, p1 = "pEuropean", p2 = "pMaori", p3 = "pAsian", breaks = 5
)
region$eth_mix_tri <- eth_mix$rgb
ggplot(region) +
geom_sf(aes(fill = eth_mix_tri)) +
scale_fill_identity() +
annotation_custom(
grob = ggplotGrob(eth_mix$key + labs(L = 'Pākehā', T = 'Māori', R = 'Asian')),
xmin = 1.7465e6, xmax = 1.7535e6, ymin = 5.9075e6, ymax = 5.9125e6)
Symbols over choropleths
Choropleth with symbols made in QGIS
Multivariate symbols
The classic example is Dorling’s Chernoff faces map of the UK 1987 election.
Figure 8.10 Dorling D. 2012. The visualisation of spatial social structure. Chichester, England: John Wiley & Sons.
Multi-element patterns
There are many variations on this idea, but perhaps the most common is a categorical dot map.
This example made using tmap and data preparation code from James Smythe’s cultureofinsight blog
dot map of ethnic composition
A woven map
weave_unit <- get_biaxial_weave_unit(spacing = 200, type = "twill", n = 3,
aspect = 0.6, ids = "ab|cd",
crs = st_crs(region))
fabric <- weave_layer(weave_unit, region, angle = 30)Split the data by the id so that it is convenient to symbolise them separately.
layers <- fabric %>% split(as.factor(fabric$id))- Each
idvalue can be symbolised separately using symbolisation the data can support - We can also plot the region data as a choropleth if desired
tm_shape(region, name = "Dose 2 uptake") +
tm_fill(col = "dose2_uptake", palette = "inferno", style = "cont",
title = "Dose 2 per 1000", id = "SA22018_V1_00_NAME") +
tm_shape(layers$a, name = "Pākehā") +
tm_fill(col = "pEuropean", palette = "Greys", title = "% Pākehā", n = 3,
id = "SA22018_V1_00_NAME") +
tm_shape(layers$b, name = "Māori") +
tm_fill(col = "pMaori", palette = "Reds", title = "% Māori", n = 3,
id = "SA22018_V1_00_NAME") +
tm_shape(layers$c, name = "Pasifika") +
tm_fill(col = "pPacific", palette = "Purples", title = "% Pasifika", n = 3,
id = "SA22018_V1_00_NAME") +
tm_shape(layers$d, name = "Asian") +
tm_fill(col = "pAsian", palette = "Greens", title = "% Asian", n = 3,
id = "SA22018_V1_00_NAME")Implementing woven maps
- Regular rectangular or hex grids of points generated by geospatial tools
tiling a weave unit
What repeatable units can tile across such grids to give the appearance of a woven pattern?
Turns out this is of interest to mathematicians (Grünbaum and Shephard 1985, 1986), who call such tileable elements the fundamental blocks of isonemal fabrics
Anyway… we have proof-of-concept R tools to make weave patterns:
- Make a weave unit
- Tile the map area with the weave unit
- Export to a multi-layer GPKG
- Symbolise the weave elements as desired in any tool
Weave units (or fundamental blocks)
Biaxial weaves
Plain weaves
Traditional weave patterns with threads in two directions, the warp and the weft. We generate these using matrix multiplication (cf. Glassner 2002)
Simplest is a plain weave
rect11_unit <- ## plain weave example
get_biaxial_weave_unit(spacing = 300, type = "plain",
ids = "a|b", crs = st_crs(region))
rect11_unit$primitive %>% plot(border = NA, main = "Plain weave unit")
This could be useful if clearly distinct palettes were used in the warp and weft elements. More useful is if we change the aspect to the warp and weft elements so we can distinguish directions:
rect11_unit <- ## plain weave example
get_biaxial_weave_unit(spacing = 300, aspect = 0.8,
ids = "a|b", crs = st_crs(region))
rect11_unit$primitive %>% plot(border = NA, main = "Plain weave unit, with directions")
More threads, more colours
This is highly customisable:
rect32_unit <-
get_biaxial_weave_unit(spacing = 150, aspect = sqrt(0.5),
ids = "abc|de", crs = st_crs(region))
rect32_unit$primitive %>% plot(border = NA, main = "Plain weave, 3 warp and 2 weft colours")
Missing threads
We can even leave gaps or duplicate threads
rect34_unit <- ## plain weave example
get_biaxial_weave_unit(spacing = 300, aspect = 0.8,
ids = "ab-|cc-d", crs = st_crs(region))
rect34_unit$primitive %>% plot(border = NA, main = "Complex plain weave with missing strands")
Twill weaves and basket weaves
twill_unit <-
get_biaxial_weave_unit(spacing = 200, type = "twill", n = c(2, 2),
aspect = 0.7, ids = "a|b", crs = st_crs(region))
twill_unit$primitive %>% plot(border = NA, main = "2 over 2 under twill weave")
basket_unit <-
get_biaxial_weave_unit(spacing = 200, type = "basket", n = 2,
aspect = 0.7, ids = "a|b", crs = st_crs(region))
basket_unit$primitive %>% plot(border = NA, main = "2 over 2 under basket weave")
Other weaves
We can generate any biaxial weavable pattern, even crazy ones!
this_unit <-
get_biaxial_weave_unit(spacing = 200, type = "this", ids = ids,
crs = st_crs(region))
this_unit$primitive %>% plot(border = NA, main = "Pattern from Glassner 2002")
Triaxial weave units
Hexagonal
We can also make weaves with threads running in 3 directions. This example is based on a hexagonal tileable unit, and can also allow for more than one thread in each direction
hex_unit <- ## hex example
get_triaxial_weave_unit(spacing = 600, margin = 2,
ids = "a|bc|def", type = "hex", crs = st_crs(region))
hex_unit$primitive %>% plot(border = NA, main = "Hex-based triangular weave")
A cube is another option, although this produces some odd 3D effects when tiled
cube_unit <-
get_triaxial_weave_unit(spacing = 600, ids = "ab|cd|ef", margin = 2,
type = "cube", crs = st_crs(region))
cube_unit$primitive %>% plot(border = NA,
main = "Mad weave (so called) with two colours in each direction")
Diamond
An alternative way to produce a triangular weave is with a diamond repeating unit with angles 60° and 120°
diamond_unit <- ## diamond example
get_triaxial_weave_unit(spacing = 600, margin = 2,
ids = "a|b|c", type = "diamond", crs = st_crs(region))
diamond_unit$primitive %>% plot(border = NA,
main = "Triangular weave with a diamond fundamental block")
Weave a map
tri_unit <- get_triaxial_weave_unit(spacing = 550, margin = 5,
ids = "a|b|c", type = "hex", crs = st_crs(region))
fabric2 <- weave_layer(tri_unit, region, angle = 45)
layers2 <- fabric2 %>% split(as.factor(fabric2$id))tmap_mode("view")
tm_shape(region, name = "Dose 2 uptake") +
tm_fill(col = "dose2_uptake", palette = "-Greys", style = "cont",
title = "Dose 2 per 1000") +
tm_shape(layers2$a, name = "Māori") +
tm_fill(col = "pMaori", palette = "Reds", title = "% Māori", n = 5) +
tm_shape(layers2$b, name = "Pasifika") +
tm_fill(col = "pPacific", palette = "Purples", title = "% Pasifika", n = 5) +
tm_shape(layers2$c, name = "Asian") +
tm_fill(col = "pAsian", palette = "Greens", title = "% Asian", n = 5)Write the weave layers
We can save out to a multi-layer GPKG for use in any tool
write_weave_layers(fabric, region, "data/fabric.gpkg")
write_weave_layers(fabric2, region, "data/fabric2.gpkg")Further work
There is lots to do (at least potentially!)
- Clarify the API for the tools to make their usage clearer
- Figure out how to make legends…
- Develop guidelines for what works (and what doesn’t)
- Figure out how colour work in this setting (how many, what combinations?)
- Explore what symbolisations work (continuous, classified, categorical?)
- Understand better how orientation operates
- Consider the use of ‘gaps’ in a weave pattern
Acknowledgments
- Thanks to co-conspirator Luke Bergmann (UBC)
- You’ll find code at github.com/DOSull/weaving-space
References
Glassner A. 2002. Digital weaving 1. IEEE Computer Graphics and Applications 22 (6):108–118.
Glassner A. 2003a. Digital weaving 2. IEEE Computer Graphics and Applications 23 (1):77–90.
Glassner A. 2003b. Digital weaving 3. IEEE Computer Graphics and Applications 23 (2):80–83.
Griswold R. 2006 (unpublished manuscript). Mathematical and Computational Topics in Weaving (last accessed 29 October 2021).
Grünbaum B and GC Shephard. 1985. A catologue of isonemal fabrics. Annals of the New York Academy of Sciences 440 (1 Discrete Geom):279–298.
Grünbaum B and GC Shephard. 1986. An extension to the catalogue of isonemal fabrics. Discrete Mathematics 60:155–192.
And this video by Lea Albaugh provided a nice way in to the topic for us: “It’s Just Matrix Multiplication”: Notation for Weaving, presented at the Strange Loop conference, St Louis, 27-28 Sept, 2018.